

package com.hazelcast.internal.partition;

import com.hazelcast.cluster.Address;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

import java.util.*;
import java.util.stream.Stream;

import static com.hazelcast.internal.partition.InternalPartition.MAX_REPLICA_COUNT;
import static com.hazelcast.internal.partition.PartitionStampUtil.calculateStamp;
import static java.util.stream.Collectors.toSet;

/**
 * An immutable/readonly view of partition table.
 * View consists of partition replica assignments and global partition state stamp.
 * <p>
 * {@link #getReplicas(int)} returns a clone of internal replica array.
 */
public class PartitionTableView {

    private final InternalPartition[] partitions;

    private long stamp;

    @SuppressFBWarnings("EI_EXPOSE_REP")
    public PartitionTableView(InternalPartition[] partitions) {
        this.partitions = partitions;
    }

    public long stamp() {
        long s = stamp;
        if (s == 0) {
            s = calculateStamp(partitions);
            stamp = s;
        }
        return s;
    }

    public int length() {
        return partitions.length;
    }

    public InternalPartition getPartition(int partitionId) {
        return partitions[partitionId];
    }

    public PartitionReplica getReplica(int partitionId, int replicaIndex) {
        InternalPartition partition = partitions[partitionId];
        return partition != null ? partition.getReplica(replicaIndex) : null;
    }

    public PartitionReplica[] getReplicas(int partitionId) {
        InternalPartition partition = partitions[partitionId];
        return partition != null ? partition.getReplicasCopy() : new PartitionReplica[MAX_REPLICA_COUNT];
    }

    /**
     * @return a measure of the difference of this
     * versus given {@code partitionTableView}.
     */
    public int distanceOf(PartitionTableView partitionTableView) {
        int distance = 0;
        for (int i = 0; i < partitions.length; i++) {
            distance += distanceOf(partitions[i], partitionTableView.partitions[i]);
        }
        return distance;
    }

    private int distanceOf(InternalPartition partition1, InternalPartition partition2) {
        int distance = 0;
        for (int i = 0; i < MAX_REPLICA_COUNT; i++) {
            PartitionReplica replica1 = partition1.getReplica(i);
            PartitionReplica replica2 = partition2.getReplica(i);
            if (replica1 == null) {
                if (replica2 != null) {
                    distance += MAX_REPLICA_COUNT;
                }
            } else {
                if (replica2 != null) {
                    if (!replica1.uuid().equals(replica2.uuid())) {
                        int replicaIndex2 = replicaIndexOfUuid(replica1.uuid(), partition2);
                        if (replicaIndex2 == -1) {
                            distance += MAX_REPLICA_COUNT;
                        } else {
                            distance += Math.abs(replicaIndex2 - i);
                        }
                    }
                }
            }
        }
        return distance;
    }

    private int replicaIndexOfUuid(UUID uuid, InternalPartition partition) {
        PartitionReplica[] replicas = ((AbstractInternalPartition) partition).replicas();
        for (int i = 0; i < replicas.length; i++) {
            if (replicas[i] != null && replicas[i].uuid().equals(uuid)) {
                return i;
            }
        }
        return -1;
    }

    public Set<UUID> getMemberUuids() {
        return Stream.of(partitions).flatMap(partition -> Stream.of(partition.getReplicasCopy())).filter(Objects::nonNull).map(PartitionReplica::uuid).collect(toSet());
    }

    public PartitionReplica[][] toArray(Map<UUID, Address> addressMap) {
        int partitionCount = partitions.length;

        PartitionReplica[][] replicas = new PartitionReplica[partitionCount][MAX_REPLICA_COUNT];

        for (int partitionId = 0; partitionId < partitionCount; partitionId++) {
            InternalPartition p = partitions[partitionId];
            replicas[partitionId] = new PartitionReplica[MAX_REPLICA_COUNT];

            for (int index = 0; index < MAX_REPLICA_COUNT; index++) {
                PartitionReplica replica = p.getReplica(index);
                if (replica == null) {
                    continue;
                }
                replicas[partitionId][index] = addressMap.containsKey(replica.uuid()) ? new PartitionReplica(addressMap.get(replica.uuid()), replica.uuid()) : replica;
            }
        }
        return replicas;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        PartitionTableView that = (PartitionTableView) o;

        return Arrays.equals(partitions, that.partitions);
    }

    @Override
    public int hashCode() {
        return Arrays.hashCode(partitions);
    }

    @Override
    public String toString() {
        return "PartitionTableView{" + "partitions=" + Arrays.toString(partitions) + ", stamp=" + stamp() + '}';
    }
}
